{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "name": "dataset_generator.ipynb",
      "provenance": [],
      "collapsed_sections": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WyBjnWt7FXsl"
      },
      "source": [
        "# Dataset generators\n",
        "\n",
        "In this advanced tutorials I demonstrate an efficient way of using the TensorFlow [tf.Data API](https://www.tensorflow.org/guide/data) to create a dataset. This approach has some important advantages:\n",
        "\n",
        "1. It provides a lot of flexibility in terms of using Python and packages such as NumPy to create a dataset.\n",
        "2. By working with large databases, you can call the samples and shuffling **on-demand** which significantly reduce memory usage. In fact, memory won't be a bottleneck anymore. \n",
        "\n",
        "This will be done by [Python generator functions](https://www.tensorflow.org/guide/data#consuming_python_generators) to create [tf.data.Dataset](https://www.tensorflow.org/api_docs/python/tf/data/Dataset) objects. The process is as follows:\n",
        "\n",
        "1. By using a generator function, we dictate the way data must be generated.\n",
        "2. By using [tf.data.Dataset.from_generator](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#from_generator) method, we create the TensorFlow dataset."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "zkYRcDwhl1UW"
      },
      "source": [
        "import tensorflow as tf\n",
        "import numpy as np"
      ],
      "execution_count": 1,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "CZqKCk9pl1rx",
        "outputId": "15f0524b-0e36-4eb7-ce60-f6cfc3cd5f86",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "# Load MNIST data\n",
        "(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()\n",
        "# Preprocessing\n",
        "x_train = x_train / 255.0\n",
        "x_test = x_test / 255.0\n",
        "\n",
        "# Add one domention to make 3D images\n",
        "x_train = x_train[...,tf.newaxis]\n",
        "x_test = x_test[...,tf.newaxis]\n",
        "\n",
        "# Track the data type\n",
        "dataType, dataShape = x_train.dtype, x_train.shape\n",
        "print(f\"Data type and shape x_train: {dataType} {dataShape}\")\n",
        "labelType, labelShape = y_train.dtype, y_train.shape\n",
        "print(f\"Data type and shape y_train: {labelType} {labelShape}\")"
      ],
      "execution_count": 2,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Data type and shape x_train: float64 (60000, 28, 28, 1)\n",
            "Data type and shape y_train: uint8 (60000,)\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3QYe8Uw0HEax"
      },
      "source": [
        "## Generators\n",
        "\n",
        "Here, I define separate generators for train/test. The generator function, pick a random sample from the dataset at each step. This create a shuffled dataset without the need to use the [.shuffle()](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#shuffle) method. Sometimes [.shuffle()](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#shuffle) method can be very [memory consuming](https://www.tensorflow.org/guide/data_performance#reducing_memory_footprint)."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "RqDCX9dnCH7Z"
      },
      "source": [
        "# Defining generator functions for train/test samples\n",
        "TRAIN_LEN = x_train.shape[0]\n",
        "def gen_pairs_train():\n",
        "    for i in range(TRAIN_LEN):\n",
        "        # Get a random image each time\n",
        "        idx = np.random.randint(0,TRAIN_LEN)\n",
        "        yield (x_train[idx], y_train[idx])\n",
        "\n",
        "\n",
        "TEST_LEN = x_test.shape[0]\n",
        "def gen_pairs_test():\n",
        "    for i in range(TEST_LEN):\n",
        "        # Get a random image each time\n",
        "        idx = np.random.randint(0,TEST_LEN)\n",
        "        yield (x_test[idx], y_test[idx])\n",
        "\n",
        "# Function to test input pipeline\n",
        "sample_image, sample_label = next(gen_pairs_train())"
      ],
      "execution_count": 3,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "CSoF5IsxH18W"
      },
      "source": [
        "## Dataset creation\n",
        "\n",
        "Here I just used tf.data.Dataset.from_generator on top of the *gen_pairs_train()* and *gen_pairs_test()* generator functions."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "XTC4nXfTCd20"
      },
      "source": [
        "batch_size = 32\n",
        "# Prepare the training dataset.\n",
        "train_dataset = tf.data.Dataset.from_generator(generator=gen_pairs_train, output_types=(tf.float64, tf.uint8))\n",
        "train_dataset = train_dataset.batch(batch_size)\n",
        "\n",
        "# Prepare the validation dataset.\n",
        "test_dataset = tf.data.Dataset.from_generator(generator=gen_pairs_test, output_types=(tf.float64, tf.uint8))\n",
        "test_dataset = test_dataset.batch(batch_size)"
      ],
      "execution_count": 4,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "6MbJ-XLIl4M2",
        "outputId": "09ef4fbb-1b31-4812-f9f7-194002762620",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 268
        }
      },
      "source": [
        "im_list = []\n",
        "n_samples_to_show = 16\n",
        "c = 0\n",
        "for i in range(n_samples_to_show):\n",
        "  img, label = next(gen_pairs_train())\n",
        "  im_list.append(img)\n",
        "# Visualization\n",
        "import matplotlib.pyplot as plt\n",
        "from mpl_toolkits.axes_grid1 import ImageGrid\n",
        "fig = plt.figure(figsize=(4., 4.))\n",
        "# Ref: https://matplotlib.org/3.1.1/gallery/axes_grid1/simple_axesgrid.html\n",
        "grid = ImageGrid(fig, 111,  # similar to subplot(111)\n",
        "                 nrows_ncols=(4, 4),  # creates 2x2 grid of axes\n",
        "                 axes_pad=0.1,  # pad between axes in inch.\n",
        "                 )\n",
        "# Show image grid\n",
        "for ax, im in zip(grid, im_list):\n",
        "    # Iterating over the grid returns the Axes.\n",
        "    ax.imshow(im[:,:,0], 'gray')\n",
        "plt.show()"
      ],
      "execution_count": 5,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 288x288 with 32 Axes>"
            ]
          },
          "metadata": {
            "tags": [],
            "needs_background": "light"
          }
        }
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "Jbj9lFh1l4iL"
      },
      "source": [
        "# Model building\n",
        "NUM_CLASSES = 10\n",
        "model = tf.keras.Sequential([\n",
        "    tf.keras.layers.Conv2D(16, (3, 3), activation='relu', input_shape=(28, 28, 1)),\n",
        "    tf.keras.layers.MaxPooling2D((2, 2)),\n",
        "    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),\n",
        "    tf.keras.layers.Flatten(),\n",
        "    tf.keras.layers.Dense(32, activation='relu'),\n",
        "    tf.keras.layers.Dense(NUM_CLASSES, activation='sigmoid')]\n",
        "    )"
      ],
      "execution_count": 6,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "PldDIuQvl7iB"
      },
      "source": [
        "# Defining loss function\n",
        "loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False)\n",
        "accuracy_metric = tf.keras.metrics.Accuracy()\n",
        "\n",
        "# Calculate loss\n",
        "def loss_fn(gt_label, pred):\n",
        "  # training argument define the beehaviour of layers with respect\n",
        "  # to whether we are training the model or not. It is important for layers\n",
        "  # such as BatchNorm and Dropout.\n",
        "  return loss_object(y_true=gt_label, y_pred=pred)\n",
        "\n",
        "def accuracy_fn(gt_label, output):\n",
        "  # calculate the accuracy by turning output into labels with argmax\n",
        "  pred = tf.argmax(output, axis=1, output_type=tf.int32)\n",
        "  return accuracy_metric(pred, gt_label)\n"
      ],
      "execution_count": 7,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "T1a1hEY6m0Ji"
      },
      "source": [
        "# Define the optimizer\n",
        "optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)"
      ],
      "execution_count": 8,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "k_2OZcLjnAhC",
        "outputId": "d4e5e3ee-27ac-4d1e-8b31-8b4e1f59521d",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "NUM_EPOCHS = 5\n",
        "EPOCH_PER_DISPLAY = 1\n",
        "total_loss = []\n",
        "for epoch in range(NUM_EPOCHS):\n",
        "\n",
        "  running_loss = []\n",
        "  running_accuracy = []\n",
        "\n",
        "  # Training\n",
        "  for input, target in train_dataset:\n",
        "\n",
        "    # Calculate and track graduents\n",
        "    with tf.GradientTape() as tape:\n",
        "\n",
        "      # Calculate model output and loss\n",
        "      output = model(input, training=True)\n",
        "      loss_ = loss_fn(target, output)\n",
        "      accuracy_ = accuracy_fn(target, output)\n",
        "      \n",
        "      # Tape gradients\n",
        "      grads = tape.gradient(loss_, model.trainable_variables)\n",
        "      \n",
        "    # Track batch loss and accuracy\n",
        "    running_loss.append(loss_)\n",
        "    running_accuracy.append(accuracy_)\n",
        "\n",
        "    # Optimize model based on the gradients\n",
        "    optimizer.apply_gradients(zip(grads, model.trainable_variables))\n",
        "  \n",
        "  # Epoch calculations\n",
        "  epoch_loss = np.mean(running_loss)\n",
        "  epoch_accuracy = np.mean(running_accuracy)\n",
        "  if (epoch + 1) % EPOCH_PER_DISPLAY == 0:\n",
        "    print(\"Epoch {}: Loss: {:.4f} Accuracy: {:.2f}%\".format(epoch+1, epoch_loss, epoch_accuracy * 100))\n"
      ],
      "execution_count": 9,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Epoch 1: Loss: 0.1329 Accuracy: 92.25%\n",
            "Epoch 2: Loss: 0.0746 Accuracy: 96.68%\n",
            "Epoch 3: Loss: 0.0679 Accuracy: 97.26%\n",
            "Epoch 4: Loss: 0.0649 Accuracy: 97.58%\n",
            "Epoch 5: Loss: 0.0695 Accuracy: 97.79%\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "Njorv726pcUW",
        "outputId": "9c0327b2-3d75-45be-916f-084e7e440970",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "# Calculate the accurcy on the test set\n",
        "running_accuracy = []\n",
        "for (input, gt_label) in test_dataset:\n",
        "  output = model(input, training=False)\n",
        "  accuracy_ = accuracy_fn(gt_label, output)\n",
        "  running_accuracy.append(accuracy_)\n",
        "\n",
        "print(\"Test accuracy: {:.3%}\".format(np.mean(running_accuracy)))\n"
      ],
      "execution_count": 10,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Test accuracy: 97.881%\n"
          ],
          "name": "stdout"
        }
      ]
    }
  ]
}